package bjut.zrpc.core.optimization.extension;

import bjut.zrpc.core.optimization.annotation.SPI;
import bjut.zrpc.core.optimization.bean.Holder;
import lombok.SneakyThrows;

import org.springframework.util.StringUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * @Author: zhuzhiming
 * @Date: 2022/6/23 19:40
 */
public class ExtensionLoader<T> {

    private static final String BASE_DIRECTORY = "META-INF/extensions/";

    private static final ConcurrentHashMap<Class<?>, Object> EXTENSION_LOADER_MAP = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Holder<T>> holderCache = new ConcurrentHashMap<>();
    private static final Holder<Map<String, Class<?>>> HOLDER_MAP = new Holder<>();
    private static final ConcurrentHashMap<Class<?>, Object> EXTENSION_INSTANCE_MAP = new ConcurrentHashMap<>();

    private static final Pattern DEFAULT_VALUE_SPLIT = Pattern.compile("\\s*[,]+\\s*");

    private String defaultValueCache;

    private Class<?> cls;

    private ExtensionLoader(Class<?> cls) {
        this.cls = cls;
    }

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> cls) {

        if (cls == null) {
            throw new NullPointerException("extension class must not be null");
        }

        if (!cls.isInterface()) {
            throw new IllegalArgumentException("extension class must be interface");
        }

        if (!cls.isAnnotationPresent(SPI.class)) {
            throw new IllegalArgumentException("extension class must has SPI annotation");
        }

        Object extensionLoader = EXTENSION_LOADER_MAP.get(cls);

        if (extensionLoader == null) {
            synchronized (ExtensionLoader.class) {
                extensionLoader = EXTENSION_LOADER_MAP.get(cls);
                if (extensionLoader == null) {
                    EXTENSION_LOADER_MAP.putIfAbsent(cls, new ExtensionLoader<T>(cls));
                    extensionLoader = EXTENSION_LOADER_MAP.get(cls);
                }
            }
        }
        return (ExtensionLoader<T>) extensionLoader;
    }


    public T getExtension(String name) {

        if (!StringUtils.hasLength(name)) {
            throw new IllegalArgumentException("extension name must be not empty or null");
        }

        if ("true".equals(name)) {
            return getDefaultExtension();
        }

        Holder<T> holder = holderCache.get(name);

        if (holder == null) {
            holderCache.putIfAbsent(name, new Holder<>());
            holder = holderCache.get(name);
        }

        T value = holder.getValue();

        if (value == null) {
            synchronized (this) {
                value = holder.getValue();
                if (value == null) {
                    value = createExtension(name);
                    holder.setValue(value);
                }
            }
        }
        return value;
    }

    private T getDefaultExtension() {

        getExtensionClass();
        if (defaultValueCache == null || defaultValueCache.length() == 0 || "true".equals(defaultValueCache)) {
            return null;
        }
        return getExtension(defaultValueCache);
    }

    @SneakyThrows
    private T createExtension(String name) {

        Class<?> aClass = getExtensionClass().get(name);

        if (aClass == null) {
            throw new RuntimeException("not find extension of " + name);
        }

        Object instance = EXTENSION_INSTANCE_MAP.get(aClass);
        if (instance == null) {
            EXTENSION_INSTANCE_MAP.putIfAbsent(aClass, aClass.newInstance());
            instance = EXTENSION_INSTANCE_MAP.get(aClass);
        }
        return (T) instance;
    }

    private Map<String, Class<?>> getExtensionClass() {

        final SPI annotation = cls.getAnnotation(SPI.class);
        if (annotation != null) {
            String defaultValue = annotation.value();
            String[] values = DEFAULT_VALUE_SPLIT.split(defaultValue.trim());
            if (values.length > 1) {
                throw new IllegalArgumentException("SPI default value length more than 1");
            }
            if (values.length == 1) {
                defaultValueCache = values[0];
            }
        }


        Map<String, Class<?>> classMap = HOLDER_MAP.getValue();
        if (classMap == null) {
            classMap = HOLDER_MAP.getValue();
            if (classMap == null) {
                classMap = new HashMap<>();
                loadDirectory(classMap);
            }
        }
        return classMap;
    }

    private void loadDirectory(Map<String, Class<?>> classMap) {

        String fileName = BASE_DIRECTORY + cls.getName();
        Enumeration<URL> resources;
        try {
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            resources = classLoader.getResources(fileName);
            URL url;
            while (resources.hasMoreElements()) {
                url = resources.nextElement();
                loadResource(classMap, url, classLoader);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    private void loadResource(Map<String, Class<?>> classMap, URL url, ClassLoader classLoader) {

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {

            String line;
            while ((line = reader.readLine()) != null) {

                int commentIndex = line.indexOf("#");
                if (commentIndex > 0) {
                    line = line.substring(0, commentIndex);
                }
                line = line.trim();
                if (line.length() > 0) {
                    int spiltIndex = line.indexOf("=");
                    if (spiltIndex > 0) {

                        String key = line.substring(0, spiltIndex);
                        String value = line.substring(spiltIndex + 1);

                        if (value.length() > 0) {
                            Class<?> aClass = classLoader.loadClass(value);
                            classMap.put(key, aClass);
                        }
                    }
                }
            }


        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e.getMessage());
        }


    }

}
